package com.gitee.cashzhang27.test.boot.crypt.mapper.interceptor;

import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN;

import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.CreateTime;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.CreateUser;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.DecryptFiled;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.Depart;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.EncryptFiled;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.LogicDelete;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.ModifiedTime;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.ModifiedUser;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.Tenant;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.persistence.Column;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;

/**
 * @author Cash Zhang
 * @version v1.0
 * @since 2019/07/03 11:27
 */
@Slf4j
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,
        Integer.class}),
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Component
public class MybatisCryptoIntercept implements Interceptor {

  private String keyword = "123";

  private static final String GETTER_PREFIX = "get";

  private final static String SELECTIVE = "Selective";

  private final static String DELETE = "delete";

  private final static String BASE_MAPPER = "com.gitee.cashzhang27.test.boot.crypt.mapper.util.CrudMapper";

  private final List<String> interceptorMethodList = ImmutableList.of(
      "insert", "insertSelective",
      "delete", "deleteByPrimaryKey",
      "updateByPrimaryKey", "updateByPrimaryKeySelective",
      "select", "selectOne", "selectByPrimaryKey", "selectByIds", "selectAll");

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Object target = invocation.getTarget();

    if (target instanceof StatementHandler) {
      StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
      // 拿到target的元数据
      MetaObject metaObject = SystemMetaObject.forObject(target);

      Object parameterObject = metaObject.getValue("parameterHandler.parameterObject");
      log.debug("sql语句用的参数是：" + parameterObject);

      MappedStatement mappedStatement = (MappedStatement) metaObject
          .getValue("delegate.mappedStatement");
      // 获取SQL命令类型
      SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();

      BoundSql boundSql = statementHandler.getBoundSql();
      //获取到原始sql语句
      String originalSql = boundSql.getSql();
      // 获取 mapper 方法名
      String namespace = mappedStatement.getId();
      String mapperMethodName = namespace.substring(namespace.lastIndexOf(".") + 1);
      // 得到实体类名
      String entityClassName = validateIntercept(namespace);
      //通过反射修改sql语句
      Field sqlField = boundSql.getClass().getDeclaredField("sql");
      if (StringUtils.isBlank(originalSql) || StringUtils.isBlank(entityClassName)) {
        return invocation.proceed();
      }

      Map<String, String> map = hasLogicDelete(entityClassName);
      String newSql = null;
      String logicDeleteColumnName = map.size() > 0 ? map.get("columnName") : null;

      if (SqlCommandType.INSERT.equals(sqlCommandType)) {
        newSql = insertProcess(parameterObject, mapperMethodName, originalSql);
        if (StringUtils.isNotBlank(newSql)) {
          sqlField.setAccessible(true);
          sqlField.set(boundSql, newSql);
        }
        return invocation.proceed();
      } else if (SqlCommandType.DELETE.equals(sqlCommandType)) {
        newSql = deleteProcess(parameterObject, mapperMethodName, originalSql,
            logicDeleteColumnName);
      } else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
        newSql = updateProcess(parameterObject, mapperMethodName, originalSql,
            logicDeleteColumnName);
      } else if (SqlCommandType.SELECT.equals(sqlCommandType)) {
        newSql = selectProcess(parameterObject, originalSql, logicDeleteColumnName);
      }
      if (StringUtils.isNotBlank(newSql)) {
        sqlField.setAccessible(true);
        sqlField.set(boundSql, newSql);
      }
      metaObject.setValue("parameterHandler.parameterObject", parameterObject);
    }
    if (target instanceof ResultSetHandler) {
      Object result = invocation.proceed();
      return postSelect(result);
    }
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
  }

  private Expression logicDeleteWithWhereCond(String columnName, Expression expression)
      throws JSQLParserException {
    if (expression == null) {
      expression = CCJSqlParserUtil.parseCondExpression(columnName + " = 0");
    } else {
      expression = CCJSqlParserUtil
          .parseCondExpression(expression.toString() + " and " + columnName + " = 0");
    }
    return expression;
  }

  private Expression dataIsolationWhereCond(String dataIsolationWhereCondStr, Expression expression)
      throws JSQLParserException {
    if (expression == null) {
      expression = CCJSqlParserUtil.parseCondExpression(dataIsolationWhereCondStr);
    } else {
      expression = CCJSqlParserUtil
          .parseCondExpression(expression.toString() + " and " + dataIsolationWhereCondStr);
    }
    return expression;
  }

  private String validateIntercept(String namespace) throws ClassNotFoundException {
    // 获取 mapper 方法名
    String mapperMethodName = namespace.substring(namespace.lastIndexOf(".") + 1);
    // 获取 mapper class
    Class<?> mapperClazz = Class.forName(namespace.replace("." + mapperMethodName, ""));
    Type[] genericInterfaces = mapperClazz.getGenericInterfaces();
    if (genericInterfaces.length > 0) {
      // 获取 mapper 父接口
      Type superInterfaces = genericInterfaces[0];
      // 判断是否继承 BaseMapper 且 使用的是拦截方法
      if (superInterfaces.getTypeName().contains(BASE_MAPPER)
          && interceptorMethodList.contains(mapperMethodName)) {
        // 得到实际的泛型
        Type type = ((ParameterizedType) superInterfaces).getActualTypeArguments()[0];
        return type.getTypeName();
      }
    }
    return null;
  }

  private byte[] generateKey(String keyword) throws NoSuchAlgorithmException {
    KeyGenerator kgen = KeyGenerator.getInstance("AES");
    kgen.init(128, new SecureRandom(keyword.getBytes()));
    SecretKey secretKey = kgen.generateKey();
    return secretKey.getEncoded();
  }


  private String insertProcess(Object parameterObject, String mapperMethodName, String originalSql)
      throws Exception {
    Field[] declaredFields = parameterObject.getClass().getDeclaredFields();
    Map<String, String> insertMap = new HashMap<>(1 << 4);
    for (Field field : declaredFields) {
      if (field.getAnnotation(CreateTime.class) != null) {
        operationTime(parameterObject, mapperMethodName, insertMap, field);
      }
      if (field.getAnnotation(CreateUser.class) != null) {
        operationUser(parameterObject, mapperMethodName, insertMap, field);
      }
      encryptBase64Value(parameterObject, field);
    }
    Insert insert = (Insert) CCJSqlParserUtil.parse(originalSql);
    List<net.sf.jsqlparser.schema.Column> columnList = insert.getColumns();
    List<Expression> expressionList = ((ExpressionList) insert.getItemsList()).getExpressions();
    for (Entry<String, String> entry : insertMap.entrySet()) {
      columnList.add((net.sf.jsqlparser.schema.Column) CCJSqlParserUtil
          .parseCondExpression(entry.getKey()));
      expressionList.add(new StringValue(entry.getValue()));
    }
    insert.setColumns(columnList);
    insert.setItemsList(new ExpressionList(expressionList));
    return insert.toString();
  }


  private String deleteProcess(Object parameterObject, String mapperMethodName, String originalSql,
      String columnName) throws Exception {

    if (Objects.equals(mapperMethodName, DELETE)) {
      Field[] declaredFields = parameterObject.getClass().getDeclaredFields();
      for (Field field : declaredFields) {
        encryptBase64Value(parameterObject, field);
      }
    }
    if (Objects.nonNull(columnName)) {
      return originalSql
          .replace("DELETE FROM", "UPDATE")
          .replace("WHERE", "SET " + columnName + " = 1 WHERE");
    }
    return null;
  }

  private String updateProcess(Object parameterObject, String mapperMethodName, String originalSql,
      String logicDeleteColumnName)
      throws Exception {
    Field[] declaredFields = parameterObject.getClass().getDeclaredFields();
    Map<String, String> updateMap = new HashMap<>(1 << 4);
    for (Field field : declaredFields) {
      if (field.getAnnotation(ModifiedTime.class) != null) {
        operationTime(parameterObject, mapperMethodName, updateMap, field);
      }
      if (field.getAnnotation(ModifiedUser.class) != null) {
        operationUser(parameterObject, mapperMethodName, updateMap, field);
      }
      encryptBase64Value(parameterObject, field);
    }
    Update update = (Update) CCJSqlParserUtil.parse(originalSql);
    List<net.sf.jsqlparser.schema.Column> columnList = update.getColumns();
    List<Expression> expressionList = update.getExpressions();
    for (Entry<String, String> entry : updateMap.entrySet()) {
      columnList.add((net.sf.jsqlparser.schema.Column) CCJSqlParserUtil
          .parseCondExpression(entry.getKey()));
      expressionList.add(new StringValue(entry.getValue()));
    }
    update.setExpressions(expressionList);
    if (Objects.nonNull(logicDeleteColumnName)) {
      Expression expression = update.getWhere();
      expression = logicDeleteWithWhereCond(logicDeleteColumnName, expression);
      update.setWhere(expression);
    }
    return update.toString();
  }

  private void operationUser(Object parameterObject, String mapperMethodName,
      Map<String, String> insertMap, Field field) throws IllegalAccessException {
    if (!mapperMethodName.contains(SELECTIVE)) {
      field.setAccessible(true);
      field.set(parameterObject, "大娃");
    } else {
      insertMap.put(getColumnName(field), "大娃");
    }
  }

  private void operationTime(Object parameterObject, String mapperMethodName,
      Map<String, String> updateMap, Field field) throws IllegalAccessException {
    if (!mapperMethodName.contains(SELECTIVE)) {
      field.setAccessible(true);
      field.set(parameterObject, LocalDateTime.now());
    } else {
      updateMap.put(getColumnName(field),
          LocalDateTime.now().format(DateTimeFormatter.ofPattern(NORM_DATETIME_PATTERN)));
    }
  }

  private final List<String> deptList = ImmutableList.of("159357", "258654");

  private String selectProcess(Object parameterObject, String originalSql,
      String logicDeleteColumnName)
      throws Exception {
    if (Objects.nonNull(parameterObject)) {
      Field[] declaredFields = parameterObject.getClass().getDeclaredFields();
      StringBuilder stringBuilder = new StringBuilder();
      for (Field field : declaredFields) {
        if (field.getAnnotation(Tenant.class) != null) {
          if (stringBuilder.length() != 0) {
            stringBuilder.append(" and ");
          }
          stringBuilder.append(getColumnName(field));
          stringBuilder.append("= ");
          stringBuilder.append("123456");
        }
        if (field.getAnnotation(Depart.class) != null) {
          if (stringBuilder.length() != 0) {
            stringBuilder.append(" and ");
          }
          stringBuilder.append(getColumnName(field));
          stringBuilder.append(" in (");
          stringBuilder
              .append(deptList.size() > 0 ? Joiner.on(",")
                  .join(deptList.stream().map(deptId -> "'" + deptId + "'")
                      .collect(Collectors.toList())) : "''");
          stringBuilder.append(" )");
        }
        encryptBase64Value(parameterObject, field);
      }
      String dataIsolationWhereCondStr = stringBuilder.toString();

      CCJSqlParserManager parserManager = new CCJSqlParserManager();
      Select select = (Select) parserManager.parse(new StringReader(originalSql));
      PlainSelect plain = (PlainSelect) select.getSelectBody();
      Expression expression = plain.getWhere();
      if (StrUtil.isNotBlank(dataIsolationWhereCondStr)) {
        expression = dataIsolationWhereCond(dataIsolationWhereCondStr, expression);
      }
      if (Objects.nonNull(logicDeleteColumnName)) {
        expression = logicDeleteWithWhereCond(logicDeleteColumnName, expression);
      }
      plain.setWhere(expression);
      return select.toString();
    }
    return null;
  }

  private void encryptBase64Value(Object parameterObject, Field field)
      throws Exception {
    if (field.getAnnotation(EncryptFiled.class) != null) {
      // 生成密钥
      byte[] key = generateKey(keyword);
      Object fieldValue = getFieldValue(parameterObject, field);
      if (Objects.nonNull(fieldValue)) {
        field.setAccessible(true);
        log.debug("加密前：" + field.getName() + "-->" + fieldValue);
        String encryptBase64Value = SecureUtil.aes(key)
            .encryptBase64(String.valueOf(fieldValue));
        log.debug("加密后：" + field.getName() + "-->" + encryptBase64Value);
        field.set(parameterObject, encryptBase64Value);
      }
    }
  }

  private String getColumnName(Field field) {
    Column column = field.getAnnotation(Column.class);
    String columnName;
    if (column != null) {
      columnName = column.name();
    } else {
      columnName = humpToUnderline(field.getName());
    }
    return columnName;
  }

  private Object postSelect(Object result) throws Exception {
    if (result instanceof ArrayList) {
      ArrayList resultList = (ArrayList) result;
      for (Object obj : resultList) {
        // 获取实体类的所有属性，返回Field数组
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
          if (field.getAnnotation(DecryptFiled.class) != null) {
            // 生成密钥
            byte[] key = generateKey(keyword);
            Object fieldValue = getFieldValue(obj, field);
            if (Objects.nonNull(fieldValue)) {
              // 可访问私有变量
              field.setAccessible(true);
              log.debug("解密前：" + field.getName() + "-->" + fieldValue);
              String decodeValue = SecureUtil.aes(key)
                  .decryptStr(Base64Utils
                      .decodeFromString(String.valueOf(getFieldValue(obj, field))));
              log.debug("解密后：" + field.getName() + "-->" + decodeValue);
              field.set(obj, decodeValue);
            }
          }
        }
      }
    }
    return result;
  }

  private Object getFieldValue(Object obj, Field field) {
    String fieldName = field.getName();
    return ReflectUtil.invoke(obj, GETTER_PREFIX + StringUtils.capitalize(fieldName));
  }

  private Map<String, String> hasLogicDelete(String entityClassName) throws ClassNotFoundException {
    Map<String, String> map = new HashMap<>(1 << 4);
    Class<?> entityClazz = Class.forName(entityClassName);
    // 获取私有属性，并判断是否含有 LogicDelete 注解
    Field[] entityFields = entityClazz.getDeclaredFields();
    for (Field field : entityFields) {
      if (field.getAnnotation(LogicDelete.class) != null) {
        Column column = field.getAnnotation(Column.class);
        if (column != null) {
          map.put("columnName", column.name());
        } else {
          map.put("columnName", humpToUnderline(field.getName()));
        }
        break;
      }
    }
    return map;
  }

  /***
   * 驼峰命名转为下划线命名
   *
   * @param para
   *        驼峰命名的字符串
   */
  private String humpToUnderline(String para) {
    StringBuilder sb = new StringBuilder(para);
    //定位
    int temp = 0;
    for (int i = 0; i < para.length(); i++) {
      if (Character.isUpperCase(para.charAt(i))) {
        sb.insert(i + temp, "_");
        temp += 1;
      }
    }
    return sb.toString().toLowerCase();
  }

}